import { Vec3 } from 'cc';
import TileOccluder from '../world/TileOccluder';

/** Axis-aligned bounding box. */
export interface AABB {
  min: Vec3;
  max: Vec3;
}

export interface TileCollision {
  isBlocked(world: Vec3): boolean;
}

export interface LoSOptions {
  aabbs?: readonly AABB[];
  tiles?: TileCollision;
  step?: number;
}

/**
 * 粗略线段 LoS 判定：与 AABB 并求离散抽样检查 Tile 阻挡。
 */
export function hasLineOfSight(from: Vec3, to: Vec3, _options: LoSOptions = {}): boolean {
  const options = _options;
  const occ = TileOccluder.get();
  let occluded = false;
  if (occ) {
    occluded = !occ.hasLineOfSight(from, to);
  }
  if (occluded) {
    if (typeof console !== 'undefined' && console.debug) {
      const a = `${from.x.toFixed(1)},${from.y.toFixed(1)}`;
      const b = `${to.x.toFixed(1)},${to.y.toFixed(1)}`;
      console.debug('[LOS] occluded by tiles', a, '->', b);
    }
    return false;
  }

  if (options.aabbs && options.aabbs.length && intersectsAnyAabb(from, to, options.aabbs)) {
    if (typeof console !== 'undefined' && console.debug) {
      const a = `${from.x.toFixed(1)},${from.y.toFixed(1)}`;
      const b = `${to.x.toFixed(1)},${to.y.toFixed(1)}`;
      console.debug('[LOS] blocked by AABB', a, '->', b);
    }
    return false;
  }

  if (options.tiles) {
    const sampleStep = options.step ?? 32;
    if (hitsBlockedTile(from, to, options.tiles, sampleStep)) {
      if (typeof console !== 'undefined' && console.debug) {
        const a = `${from.x.toFixed(1)},${from.y.toFixed(1)}`;
        const b = `${to.x.toFixed(1)},${to.y.toFixed(1)}`;
        console.debug('[LOS] blocked by custom tiles', a, '->', b);
      }
      return false;
    }
  }

  if (typeof console !== 'undefined' && console.debug) {
    const a = `${from.x.toFixed(1)},${from.y.toFixed(1)}`;
    const b = `${to.x.toFixed(1)},${to.y.toFixed(1)}`;
    console.debug('[LOS] ray', a, '->', b, 'result= true');
  }
  return true;
}

// Spec alias: keep a short name for future swaps
export const hasLoS = hasLineOfSight;

export default {
  hasLineOfSight,
  hasLoS,
};

function intersectsAnyAabb(from: Vec3, to: Vec3, boxes: readonly AABB[]): boolean {
  return boxes.some((box) => segmentIntersectsAabb(from, to, box));
}

function segmentIntersectsAabb(from: Vec3, to: Vec3, box: AABB): boolean {
  let tmin = 0;
  let tmax = 1;
  const dir = new Vec3(to.x - from.x, to.y - from.y, to.z - from.z);
  const axes: Array<keyof Vec3> = ['x', 'y', 'z'];

  for (const axis of axes) {
    const origin = from[axis];
    const direction = dir[axis];
    const min = box.min[axis];
    const max = box.max[axis];

    if (Math.abs(direction) < 1e-6) {
      if (origin < min || origin > max) {
        return false;
      }
      continue;
    }

    const ood = 1 / direction;
    let t1 = (min - origin) * ood;
    let t2 = (max - origin) * ood;
    if (t1 > t2) {
      [t1, t2] = [t2, t1];
    }
    tmin = Math.max(tmin, t1);
    tmax = Math.min(tmax, t2);
    if (tmin > tmax) {
      return false;
    }
  }

  return true;
}

function hitsBlockedTile(
  from: Vec3,
  to: Vec3,
  tiles: TileCollision,
  step: number,
): boolean {
  const totalDistance = Vec3.distance(from, to);
  if (totalDistance === 0) {
    return tiles.isBlocked(from.clone());
  }

  const direction = new Vec3(to.x - from.x, to.y - from.y, to.z - from.z);
  direction.normalize();

  const samples = Math.ceil(totalDistance / step);
  const probe = new Vec3();
  for (let i = 1; i <= samples; i++) {
    probe.x = from.x + direction.x * step * i;
    probe.y = from.y + direction.y * step * i;
    probe.z = from.z + direction.z * step * i;
    if (tiles.isBlocked(probe)) {
      return true;
    }
  }
  return false;
}
